Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Sonar / synapse-sonar / app / api / assets / [id] / route.ts 1581 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireTenantContext, HttpError } from "@/lib/auth";
import { assertCan } from "@/lib/rbac";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";

const PatchSchema = z.object({
  offline: z.boolean(),
});

/**
 * PATCH /api/assets/:id
 *
 * Operator controls for a single asset. Currently: manually toggle the node's
 * offline state. Tenant-scoped and gated behind asset:write (Analyst+).
 */
export async function PATCH(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const ctx = await requireTenantContext();
    assertCan(ctx.role, "asset:write");

    const { offline } = PatchSchema.parse(await req.json());

    // updateMany with the tenant guard guarantees no cross-tenant write.
    const result = await prisma.asset.updateMany({
      where: { id: params.id, tenantId: ctx.tenantId },
      data: { manualOffline: offline },
    });
    if (result.count === 0) {
      return NextResponse.json({ error: "Asset not found" }, { status: 404 });
    }
    return NextResponse.json({ ok: true, offline });
  } catch (err) {
    if (err instanceof HttpError) {
      return NextResponse.json({ error: err.message }, { status: err.status });
    }
    if (err instanceof z.ZodError) {
      return NextResponse.json({ error: "Invalid input", details: err.issues }, { status: 422 });
    }
    const status = (err as { status?: number }).status ?? 500;
    return NextResponse.json({ error: (err as Error).message }, { status });
  }
}